﻿//-----------------------------------------------------------------------
// <copyright company="Microsoft Corporation">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace Microsoft.DevDiv.Wpf.Samples
{
    using System;
    using System.Diagnostics;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Markup;

    /// <summary>
    /// MarkupExtension that can defer providing a value
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public abstract class DeferredMarkupExtension<T> : MarkupExtension
    {
        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            object result = default(T);

            if (serviceProvider != null)
            {
                IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
                if (provideValueTarget != null)
                {
                    IProvideValueTargetSetter<T> setter;
                    if (TryGetFromProvideValueTarget(provideValueTarget, out setter))
                    {
                        Debug.Assert(setter != null, "setter must not be null");

                        result = this.BeginProvideValue(serviceProvider, setter);
                    }
                    else
                    {
                        // Special case: We are in a data template that will be shared across several elements
                        // return the markup extension itself so it can be cached in the template and reused during template expansion
                        result = this;
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Provides an IProvideValueTargetSetter to set the defered value when it becomes available
        /// </summary>
        /// <remarks>
        /// IProvideValueTargetSetter will typically keep the target object alive leading to leaks
        /// To prevent leaks implementations should either
        ///  x) Dispose the setter when a value is provided or the defered operation is canceled.
        ///  x) Store the setter as a weak reference.
        ///  x) Release references to the setter.
        /// </remarks>
        /// <param name="serviceProvider">Service provider passed to ProvideValue</param>
        /// <param name="setter">Setter used to apply the defered value when available</param>
        /// <returns>Place holder to use while deferred value is computed</returns>
        protected abstract T BeginProvideValue(IServiceProvider serviceProvider, IProvideValueTargetSetter<T> setter);

        private static bool TryGetFromProvideValueTarget(IProvideValueTarget target, out IProvideValueTargetSetter<T> result)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            do
            {
                DependencyObject dependencyObject = target.TargetObject as DependencyObject;
                DependencyProperty dependencyProperty = target.TargetProperty as DependencyProperty;

                if (dependencyObject != null && dependencyProperty != null)
                {
                    result = new DependencyPropertyProvideValueTargetSetter<T>(dependencyObject, dependencyProperty);
                    break;
                }

                PropertyInfo propInfo = target.TargetProperty as PropertyInfo;
                if (propInfo != null)
                {
                    result = new ClrPropertyProvideValueTargetSetter<T>(target.TargetObject, propInfo);
                    break;
                }

                FieldInfo fieldInfo = target.TargetProperty as FieldInfo;
                if (fieldInfo != null)
                {
                    result = new ClrFieldProvideValueTargetSetter<T>(target.TargetObject, fieldInfo);
                    break;
                }

                // We have no idea how to deal with the object and its property
                // this is probably template voodoo
                result = null;
            }
            while (false);

            return result != null;
        }
    }
}
